﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Global

/* XGraphicsPdfRenderer.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

#define ITALIC_SIMULATION

using System;
using System.Diagnostics;
using System.Globalization;
using System.Collections.Generic;
using System.Text;

using AM;

using PdfSharpCore.Internal;
using PdfSharpCore.Pdf;
using PdfSharpCore.Pdf.Internal;
using PdfSharpCore.Pdf.Advanced;

#endregion

#nullable enable

namespace PdfSharpCore.Drawing.Pdf;

/// <summary>
/// Represents a drawing surface for PdfPages.
/// </summary>
internal class XGraphicsPdfRenderer
    : IXGraphicsRenderer
{
    #region Construction

    /// <summary>
    ///
    /// </summary>
    /// <param name="page"></param>
    /// <param name="graphics"></param>
    /// <param name="options"></param>
    public XGraphicsPdfRenderer
        (
            PdfPage page,
            XGraphics graphics,
            XGraphicsPdfPageOptions options
        )
    {
        Sure.NotNull (page);

        _page = page;

        var document = page._document.ThrowIfNull();
        _colorMode = document.Options.ColorMode;
        PageOptions = options;
        Graphics = graphics;
        _content = new StringBuilder();
        page.RenderContent!._pdfRenderer = this;
        _gfxState = new PdfGraphicsState (this);
    }

    /// <summary>
    ///
    /// </summary>
    /// <param name="form"></param>
    /// <param name="graphics"></param>
    public XGraphicsPdfRenderer
        (
            XForm form,
            XGraphics graphics
        )
    {
        _form = form;
        _colorMode = form.Owner.Options.ColorMode;
        Graphics = graphics;
        _content = new StringBuilder();
        form.PdfRenderer = this;
        _gfxState = new PdfGraphicsState (this);
    }

    #endregion

    /// <summary>
    /// Gets the content created by this renderer.
    /// </summary>
    string GetContent()
    {
        EndPage();
        return _content.ToString();
    }

    public XGraphicsPdfPageOptions PageOptions { get; }

    public void Close()
    {
        if (_page != null)
        {
            var content2 = _page.RenderContent;
            content2!.CreateStream (PdfEncoders.RawEncoding.GetBytes (GetContent()));

            Graphics = null;
            _page.RenderContent!._pdfRenderer = null;
            _page.RenderContent = null;
            _page = null;
        }
        else if (_form != null)
        {
            _form._pdfForm!.CreateStream (PdfEncoders.RawEncoding.GetBytes (GetContent()));
            Graphics = null;
            _form.PdfRenderer = null;
            _form = null;
        }
    }

    // --------------------------------------------------------------------------------------------

    #region Drawing

    //void SetPageLayout(down, point(0, 0), unit

    // ----- DrawLine -----------------------------------------------------------------------------

    /// <summary>
    /// Strokes a single connection of two points.
    /// </summary>
    public void DrawLine
        (
            XPen pen,
            double x1,
            double y1,
            double x2,
            double y2
        )
    {
        DrawLines (pen, new XPoint[] { new (x1, y1), new (x2, y2) });
    }

    // ----- DrawLines ----------------------------------------------------------------------------

    /// <summary>
    /// Strokes a series of connected points.
    /// </summary>
    public void DrawLines
        (
            XPen pen,
            XPoint[] points
        )
    {
        Sure.NotNull (pen);
        Sure.NotNull (points);

        var count = points.Length;
        if (count == 0)
        {
            return;
        }

        Realize (pen);

        const string format = Config.SignificantFigures4;
        AppendFormatPoint ("{0:" + format + "} {1:" + format + "} m\n", points[0].X, points[0].Y);
        for (var idx = 1; idx < count; idx++)
        {
            AppendFormatPoint ("{0:" + format + "} {1:" + format + "} l\n", points[idx].X, points[idx].Y);
        }

        _content.Append ("S\n");
    }

    // ----- DrawBezier ---------------------------------------------------------------------------

    public void DrawBezier
        (
            XPen pen,
            double x1,
            double y1,
            double x2,
            double y2,
            double x3,
            double y3,
            double x4,
            double y4
        )
    {
        DrawBeziers
            (
                pen,
                new XPoint[] { new (x1, y1), new (x2, y2), new (x3, y3), new (x4, y4) }
            );
    }

    // ----- DrawBeziers --------------------------------------------------------------------------

    public void DrawBeziers
        (
            XPen pen,
            XPoint[] points
        )
    {
        Sure.NotNull (pen);
        Sure.NotNull (points);

        var count = points.Length;
        if (count == 0)
        {
            return;
        }

        if ((count - 1) % 3 != 0)
        {
            throw new ArgumentException ("Invalid number of points for bezier curves. Number must fulfil 4+3n.",
                nameof (points));
        }

        Realize (pen);

        const string format = Config.SignificantFigures4;
        AppendFormatPoint ("{0:" + format + "} {1:" + format + "} m\n", points[0].X, points[0].Y);
        for (var idx = 1; idx < count; idx += 3)
        {
            AppendFormat3Points (
                "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format +
                "} {5:" + format + "} c\n",
                points[idx].X, points[idx].Y,
                points[idx + 1].X, points[idx + 1].Y,
                points[idx + 2].X, points[idx + 2].Y);
        }

        AppendStrokeFill (pen, null, XFillMode.Alternate, false);
    }

    // ----- DrawCurve ----------------------------------------------------------------------------

    public void DrawCurve
        (
            XPen pen,
            XPoint[] points,
            double tension
        )
    {
        Sure.NotNull (pen);
        Sure.NotNull (points);

        var count = points.Length;
        if (count == 0)
        {
            return;
        }

        if (count < 2)
        {
            throw new ArgumentException ("Not enough points", nameof (points));
        }

        // See http://pubpages.unh.edu/~cs770/a5/cardinal.html  // Link is down...
        tension /= 3;

        Realize (pen);

        const string format = Config.SignificantFigures4;
        AppendFormatPoint ("{0:" + format + "} {1:" + format + "} m\n", points[0].X, points[0].Y);
        if (count == 2)
        {
            // Just draws a line.
            AppendCurveSegment (points[0], points[0], points[1], points[1], tension);
        }
        else
        {
            AppendCurveSegment (points[0], points[0], points[1], points[2], tension);
            for (var idx = 1; idx < count - 2; idx++)
            {
                AppendCurveSegment (points[idx - 1], points[idx], points[idx + 1], points[idx + 2], tension);
            }

            AppendCurveSegment (points[count - 3], points[count - 2], points[count - 1], points[count - 1],
                tension);
        }

        AppendStrokeFill (pen, null, XFillMode.Alternate, false);
    }

    // ----- DrawArc ------------------------------------------------------------------------------

    public void DrawArc
        (
            XPen pen,
            double x,
            double y,
            double width,
            double height,
            double startAngle,
            double sweepAngle
        )
    {
        if (pen == null)
        {
            throw new ArgumentNullException (nameof (pen));
        }

        Realize (pen);

        AppendPartialArc (x, y, width, height, startAngle, sweepAngle, PathStart.MoveTo1st, new XMatrix());
        AppendStrokeFill (pen, null, XFillMode.Alternate, false);
    }

    // ----- DrawRectangle ------------------------------------------------------------------------

    public void DrawRectangle
        (
            XPen pen,
            XBrush brush,
            double x,
            double y,
            double width,
            double height
        )
    {
        if (pen is null && brush is null)
        {
            throw new ArgumentNullException ("pen and brush");
        }

        const string format = Config.SignificantFigures3;

        Realize (pen, brush);

        //AppendFormat123("{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} re\n", x, y, width, -height);
        AppendFormatRect ("{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} re\n", x,
            y + height, width, height);

        if (pen != null && brush != null)
        {
            _content.Append ("B\n");
        }
        else if (pen != null)
        {
            _content.Append ("S\n");
        }
        else
        {
            _content.Append ("f\n");
        }
    }

    // ----- DrawRectangles -----------------------------------------------------------------------

    public void DrawRectangles
        (
            XPen pen,
            XBrush brush,
            XRect[] rects
        )
    {
        var count = rects.Length;
        for (var idx = 0; idx < count; idx++)
        {
            var rect = rects[idx];
            DrawRectangle (pen, brush, rect.X, rect.Y, rect.Width, rect.Height);
        }
    }

    // ----- DrawRoundedRectangle -----------------------------------------------------------------

    public void DrawRoundedRectangle
        (
            XPen pen,
            XBrush brush,
            double x,
            double y,
            double width,
            double height,
            double ellipseWidth,
            double ellipseHeight
        )
    {
        var path = new XGraphicsPath();
        path.AddRoundedRectangle (x, y, width, height, ellipseWidth, ellipseHeight);
        DrawPath (pen, brush, path);
    }

    // ----- DrawEllipse --------------------------------------------------------------------------

    public void DrawEllipse
        (
            XPen pen,
            XBrush brush,
            double x,
            double y,
            double width,
            double height
        )
    {
        Realize (pen, brush);

        // Useful information is here http://home.t-online.de/home/Robert.Rossmair/ellipse.htm (note: link was dead on November 2, 2015)
        // or here http://www.whizkidtech.redprince.net/bezier/circle/
        // Deeper but more difficult: http://www.tinaja.com/cubic01.asp
        var rect = new XRect (x, y, width, height);
        var δx = rect.Width / 2;
        var δy = rect.Height / 2;
        var fx = δx * Const.κ;
        var fy = δy * Const.κ;
        var x0 = rect.X + δx;
        var y0 = rect.Y + δy;

        // Approximate an ellipse by drawing four cubic splines.
        const string format = Config.SignificantFigures4;
        AppendFormatPoint ("{0:" + format + "} {1:" + format + "} m\n", x0 + δx, y0);
        AppendFormat3Points (
            "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format + "} {5:" +
            format + "} c\n",
            x0 + δx, y0 + fy, x0 + fx, y0 + δy, x0, y0 + δy);
        AppendFormat3Points (
            "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format + "} {5:" +
            format + "} c\n",
            x0 - fx, y0 + δy, x0 - δx, y0 + fy, x0 - δx, y0);
        AppendFormat3Points (
            "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format + "} {5:" +
            format + "} c\n",
            x0 - δx, y0 - fy, x0 - fx, y0 - δy, x0, y0 - δy);
        AppendFormat3Points (
            "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format + "} {5:" +
            format + "} c\n",
            x0 + fx, y0 - δy, x0 + δx, y0 - fy, x0 + δx, y0);
        AppendStrokeFill (pen, brush, XFillMode.Winding, true);
    }

    // ----- DrawPolygon --------------------------------------------------------------------------

    public void DrawPolygon
        (
            XPen pen,
            XBrush brush,
            XPoint[] points,
            XFillMode fillmode
        )
    {
        Realize (pen, brush);

        var count = points.Length;
        if (points.Length < 2)
        {
            throw new ArgumentException ("points", PSSR.PointArrayAtLeast (2));
        }

        const string format = Config.SignificantFigures4;
        AppendFormatPoint ("{0:" + format + "} {1:" + format + "} m\n", points[0].X, points[0].Y);
        for (var idx = 1; idx < count; idx++)
        {
            AppendFormatPoint ("{0:" + format + "} {1:" + format + "} l\n", points[idx].X, points[idx].Y);
        }

        AppendStrokeFill (pen, brush, fillmode, true);
    }

    // ----- DrawPie ------------------------------------------------------------------------------

    public void DrawPie
        (
            XPen pen,
            XBrush brush,
            double x,
            double y,
            double width,
            double height,
            double startAngle,
            double sweepAngle
        )
    {
        Realize (pen, brush);

        const string format = Config.SignificantFigures4;
        AppendFormatPoint ("{0:" + format + "} {1:" + format + "} m\n", x + width / 2, y + height / 2);
        AppendPartialArc (x, y, width, height, startAngle, sweepAngle, PathStart.LineTo1st, new XMatrix());
        AppendStrokeFill (pen, brush, XFillMode.Alternate, true);
    }

    // ----- DrawClosedCurve ----------------------------------------------------------------------

    public void DrawClosedCurve
        (
            XPen pen,
            XBrush brush,
            XPoint[] points,
            double tension,
            XFillMode fillmode
        )
    {
        var count = points.Length;
        if (count == 0)
        {
            return;
        }

        if (count < 2)
        {
            throw new ArgumentException ("Not enough points.", nameof (points));
        }

        // Simply tried out. Not proofed why it is correct.
        tension /= 3;

        Realize (pen, brush);

        const string format = Config.SignificantFigures4;
        AppendFormatPoint ("{0:" + format + "} {1:" + format + "} m\n", points[0].X, points[0].Y);
        if (count == 2)
        {
            // Just draw a line.
            AppendCurveSegment (points[0], points[0], points[1], points[1], tension);
        }
        else
        {
            AppendCurveSegment (points[count - 1], points[0], points[1], points[2], tension);
            for (var idx = 1; idx < count - 2; idx++)
            {
                AppendCurveSegment (points[idx - 1], points[idx], points[idx + 1], points[idx + 2], tension);
            }

            AppendCurveSegment (points[count - 3], points[count - 2], points[count - 1], points[0], tension);
            AppendCurveSegment (points[count - 2], points[count - 1], points[0], points[1], tension);
        }

        AppendStrokeFill (pen, brush, fillmode, true);
    }

    // ----- DrawPath -----------------------------------------------------------------------------

    public void DrawPath
        (
            XPen pen,
            XBrush brush,
            XGraphicsPath path
        )
    {
        if (pen is null && brush is null)
        {
            throw new ArgumentNullException (nameof (pen));
        }

        Realize (pen, brush);
        AppendPath (path._corePath);
        AppendStrokeFill (pen, brush, path.FillMode, false);
    }

    // ----- DrawString ---------------------------------------------------------------------------

    public void DrawString
        (
            string s,
            XFont font,
            XBrush brush,
            XRect rect,
            XStringFormat format
        )
    {
        var x = rect.X;
        var y = rect.Y;

        var lineSpace = font.GetHeight();
        var cyAscent = lineSpace * font.CellAscent / font.CellSpace;
        var cyDescent = lineSpace * font.CellDescent / font.CellSpace;
        var width = Graphics!.MeasureString (s, font).Width;

        //bool bold = (font.Style & XFontStyle.Bold) != 0;
        //bool italic = (font.Style & XFontStyle.Italic) != 0;
        var italicSimulation = (font.GlyphTypeface.StyleSimulations & XStyleSimulations.ItalicSimulation) != 0;
        var boldSimulation = (font.GlyphTypeface.StyleSimulations & XStyleSimulations.BoldSimulation) != 0;
        var strikeout = (font.Style & XFontStyle.Strikeout) != 0;
        var underline = (font.Style & XFontStyle.Underline) != 0;

        Realize (font, brush, boldSimulation ? 2 : 0);

        switch (format.Alignment)
        {
            case XStringAlignment.Near:
                // nothing to do
                break;

            case XStringAlignment.Center:
                x += (rect.Width - width) / 2;
                break;

            case XStringAlignment.Far:
                x += rect.Width - width;
                break;
        }

        if (Graphics.PageDirection == XPageDirection.Downwards)
        {
            switch (format.LineAlignment)
            {
                case XLineAlignment.Near:
                    y += cyAscent;
                    break;

                case XLineAlignment.Center:
                    // TODO: Use CapHeight. PDFlib also uses 3/4 of ascent
                    y += (cyAscent * 3 / 4) / 2 + rect.Height / 2;
                    break;

                case XLineAlignment.Far:
                    y += -cyDescent + rect.Height;
                    break;

                case XLineAlignment.BaseLine:
                    // Nothing to do.
                    break;
            }
        }
        else
        {
            switch (format.LineAlignment)
            {
                case XLineAlignment.Near:
                    y += cyDescent;
                    break;

                case XLineAlignment.Center:
                    // TODO: Use CapHeight. PDFlib also uses 3/4 of ascent
                    y += -(cyAscent * 3 / 4) / 2 + rect.Height / 2;
                    break;

                case XLineAlignment.Far:
                    y += -cyAscent + rect.Height;
                    break;

                case XLineAlignment.BaseLine:
                    // Nothing to do.
                    break;
            }
        }

        var realizedFont = _gfxState._realizedFont;
        Debug.Assert (realizedFont != null);
        realizedFont.AddChars (s);

        const string format2 = Config.SignificantFigures4;
        var descriptor = realizedFont.FontDescriptor._descriptor;

        string? text;
        if (font.Unicode)
        {
            var sb = new StringBuilder();
            var isSymbolFont = descriptor.FontFace._cmap!.symbol;
            for (var idx = 0; idx < s.Length; idx++)
            {
                var ch = s[idx];
                if (isSymbolFont)
                {
                    // Remap ch for symbol fonts.
                    ch = (char)(ch | (descriptor.FontFace._os2!.usFirstCharIndex & 0xFF00)); // @@@ refactor
                }

                var glyphID = descriptor.CharCodeToGlyphIndex (ch);
                sb.Append ((char)glyphID);
            }

            s = sb.ToString();

            var bytes = PdfEncoders.RawUnicodeEncoding.GetBytes (s);
            bytes = PdfEncoders.FormatStringLiteral (bytes, true, false, true, null);
            text = PdfEncoders.RawEncoding.GetString (bytes, 0, bytes.Length);
        }
        else
        {
            var bytes = PdfEncoders.WinAnsiEncoding.GetBytes (s);
            text = PdfEncoders.ToStringLiteral (bytes, false, null);
        }

        // Map absolute position to PDF world space.
        var pos = new XPoint (x, y);
        pos = WorldToView (pos);

        double verticalOffset = 0;
        if (boldSimulation)
        {
            // Adjust baseline in case of bold simulation???
            // No, because this would change the center of the glyphs.
            //verticalOffset = font.Size * Const.BoldEmphasis / 2;
        }

#if ITALIC_SIMULATION
        if (italicSimulation)
        {
            if (_gfxState.ItalicSimulationOn)
            {
                AdjustTdOffset (ref pos, verticalOffset, true);
                AppendFormatArgs ("{0:" + format2 + "} {1:" + format2 + "} Td\n{2} Tj\n", pos.X, pos.Y, text);
            }
            else
            {
                // Italic simulation is done by skewing characters 20° to the right.
                var m = new XMatrix (1, 0, Const.ItalicSkewAngleSinus, 1, pos.X, pos.Y);
                AppendFormatArgs (
                    "{0:" + format2 + "} {1:" + format2 + "} {2:" + format2 + "} {3:" + format2 + "} {4:" +
                    format2 + "} {5:" + format2 + "} Tm\n{6} Tj\n",
                    m.M11, m.M12, m.M21, m.M22, m.OffsetX, m.OffsetY, text);
                _gfxState.ItalicSimulationOn = true;
                AdjustTdOffset (ref pos, verticalOffset, false);
            }
        }
        else
        {
            if (_gfxState.ItalicSimulationOn)
            {
                var m = new XMatrix (1, 0, 0, 1, pos.X, pos.Y);
                AppendFormatArgs (
                    "{0:" + format2 + "} {1:" + format2 + "} {2:" + format2 + "} {3:" + format2 + "} {4:" +
                    format2 + "} {5:" + format2 + "} Tm\n{6} Tj\n",
                    m.M11, m.M12, m.M21, m.M22, m.OffsetX, m.OffsetY, text);
                _gfxState.ItalicSimulationOn = false;
                AdjustTdOffset (ref pos, verticalOffset, false);
            }
            else
            {
                AdjustTdOffset (ref pos, verticalOffset, false);
                AppendFormatArgs ("{0:" + format2 + "} {1:" + format2 + "} Td {2} Tj\n", pos.X, pos.Y, text);
            }
        }
#else
                AdjustTextMatrix(ref pos);
                AppendFormat2("{0:" + format2 + "} {1:" + format2 + "} Td {2} Tj\n", pos.X, pos.Y, text);
#endif
        if (underline)
        {
            var underlinePosition = lineSpace * realizedFont.FontDescriptor._descriptor.UnderlinePosition /
                                    font.CellSpace;
            var underlineThickness = lineSpace * realizedFont.FontDescriptor._descriptor.UnderlineThickness /
                                     font.CellSpace;

            //DrawRectangle(null, brush, x, y - underlinePosition, width, underlineThickness);
            var underlineRectY = Graphics.PageDirection == XPageDirection.Downwards
                ? y - underlinePosition
                : y + underlinePosition - underlineThickness;
            DrawRectangle (null, brush, x, underlineRectY, width, underlineThickness);
        }

        if (strikeout)
        {
            var strikeoutPosition = lineSpace * realizedFont.FontDescriptor._descriptor.StrikeoutPosition /
                                    font.CellSpace;
            var strikeoutSize =
                lineSpace * realizedFont.FontDescriptor._descriptor.StrikeoutSize / font.CellSpace;

            //DrawRectangle(null, brush, x, y - strikeoutPosition - strikeoutSize, width, strikeoutSize);
            var strikeoutRectY = Graphics.PageDirection == XPageDirection.Downwards
                ? y - strikeoutPosition
                : y + strikeoutPosition - strikeoutSize;
            DrawRectangle (null, brush, x, strikeoutRectY, width, strikeoutSize);
        }
    }

    // ----- DrawImage ----------------------------------------------------------------------------

    //public void DrawImage(Image image, Point point);
    //public void DrawImage(Image image, PointF point);
    //public void DrawImage(Image image, Point[] destPoints);
    //public void DrawImage(Image image, PointF[] destPoints);
    //public void DrawImage(Image image, Rectangle rect);
    //public void DrawImage(Image image, RectangleF rect);
    //public void DrawImage(Image image, int x, int y);
    //public void DrawImage(Image image, float x, float y);
    //public void DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit);
    //public void DrawImage(Image image, Rectangle destRect, Rectangle srcRect, GraphicsUnit srcUnit);
    //public void DrawImage(Image image, RectangleF destRect, RectangleF srcRect, GraphicsUnit srcUnit);
    //public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit);
    //public void DrawImage(Image image, int x, int y, Rectangle srcRect, GraphicsUnit srcUnit);
    //public void DrawImage(Image image, float x, float y, RectangleF srcRect, GraphicsUnit srcUnit);
    //public void DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr);
    //public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr);
    //public void DrawImage(Image image, int x, int y, int width, int height);
    //public void DrawImage(Image image, float x, float y, float width, float height);
    //public void DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback);
    //public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback);
    //public void DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit);
    //public void DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit);
    //public void DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback, int callbackData);
    //public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback, int callbackData);
    //public void DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttr);
    //public void DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs);
    //public void DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttr, DrawImageAbort callback);
    //public void DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs, DrawImageAbort callback);
    //public void DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, ImageAttributes imageAttrs, DrawImageAbort callback, IntPtr callbackData);
    //public void DrawImage(Image image, Rectangle destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, ImageAttributes

    public void DrawImage
        (
            XImage image,
            double x,
            double y,
            double width,
            double height
        )
    {
        const string format = Config.SignificantFigures4;

        var name = Realize (image);
        if (image is not XForm)
        {
            if (Graphics!.PageDirection == XPageDirection.Downwards)
            {
                AppendFormatImage (
                    "q {2:" + format + "} 0 0 {3:" + format + "} {0:" + format + "} {1:" + format +
                    "} cm {4} Do Q\n",
                    x, y + height, width, height, name);
            }
            else
            {
                AppendFormatImage (
                    "q {2:" + format + "} 0 0 {3:" + format + "} {0:" + format + "} {1:" + format +
                    "} cm {4} Do Q\n",
                    x, y, width, height, name);
            }
        }
        else
        {
            BeginPage();

            var form = (XForm)image;
            form.Finish();

            var pdfForm = Owner!.FormTable.GetForm (form);

            var cx = width / image.PointWidth;
            var cy = height / image.PointHeight;

            if (cx != 0 && cy != 0)
            {
                var xForm = image as XPdfForm;
                if (Graphics!.PageDirection == XPageDirection.Downwards)
                {
                    // If we have an XPdfForm, then we take the MediaBox into account.
                    var xDraw = x;
                    var yDraw = y;
                    if (xForm != null)
                    {
                        // Yes, it is an XPdfForm - adjust the position where the page will be drawn.
                        xDraw -= xForm.Page.MediaBox.X1;
                        yDraw += xForm.Page.MediaBox.Y1;
                    }

                    AppendFormatImage (
                        "q {2:" + format + "} 0 0 {3:" + format + "} {0:" + format + "} {1:" + format +
                        "} cm 100 Tz {4} Do Q\n",
                        xDraw, yDraw + height, cx, cy, name);
                }
                else
                {
                    // TODO Translation for MediaBox.
                    AppendFormatImage (
                        "q {2:" + format + "} 0 0 {3:" + format + "} {0:" + format + "} {1:" + format +
                        "} cm {4} Do Q\n",
                        x, y, cx, cy, name);
                }
            }
        }
    }

    // TODO: incomplete - srcRect not used
    public void DrawImage
        (
            XImage image,
            XRect destRect,
            XRect srcRect,
            XGraphicsUnit srcUnit
        )
    {
        const string format = Config.SignificantFigures4;

        var x = destRect.X;
        var y = destRect.Y;
        var width = destRect.Width;
        var height = destRect.Height;

        var name = Realize (image);
        if (image is not XForm)
        {
            if (Graphics!.PageDirection == XPageDirection.Downwards)
            {
                AppendFormatImage (
                    "q {2:" + format + "} 0 0 {3:" + format + "} {0:" + format + "} {1:" + format +
                    "} cm {4} Do\nQ\n",
                    x, y + height, width, height, name);
            }
            else
            {
                AppendFormatImage (
                    "q {2:" + format + "} 0 0 {3:" + format + "} {0:" + format + "} {1:" + format +
                    "} cm {4} Do Q\n",
                    x, y, width, height, name);
            }
        }
        else
        {
            BeginPage();

            var form = (XForm)image;
            form.Finish();

            var pdfForm = Owner!.FormTable.GetForm (form);

            var cx = width / image.PointWidth;
            var cy = height / image.PointHeight;

            if (cx != 0 && cy != 0)
            {
                var xForm = image as XPdfForm;
                if (Graphics!.PageDirection == XPageDirection.Downwards)
                {
                    var xDraw = x;
                    var yDraw = y;
                    if (xForm != null)
                    {
                        // Yes, it is an XPdfForm - adjust the position where the page will be drawn.
                        xDraw -= xForm.Page.MediaBox.X1;
                        yDraw += xForm.Page.MediaBox.Y1;
                    }

                    AppendFormatImage (
                        "q {2:" + format + "} 0 0 {3:" + format + "} {0:" + format + "} {1:" + format +
                        "} cm {4} Do Q\n",
                        xDraw, yDraw + height, cx, cy, name);
                }
                else
                {
                    // TODO Translation for MediaBox.
                    AppendFormatImage (
                        "q {2:" + format + "} 0 0 {3:" + format + "} {0:" + format + "} {1:" + format +
                        "} cm {4} Do Q\n",
                        x, y, cx, cy, name);
                }
            }
        }
    }

    #endregion

    // --------------------------------------------------------------------------------------------

    #region Save and Restore

    /// <summary>
    /// Clones the current graphics state and push it on a stack.
    /// </summary>
    public void Save
        (
            XGraphicsState state
        )
    {
        // Before saving, the current transformation matrix must be completely realized.
        BeginGraphicMode();
        RealizeTransform();

        // Associate the XGraphicsState with the current PdgGraphicsState.
        _gfxState.InternalState = state.InternalState;
        SaveState();
    }

    public void Restore
        (
            XGraphicsState state
        )
    {
        BeginGraphicMode();
        RestoreState (state.InternalState);
    }

    public void BeginContainer
        (
            XGraphicsContainer container,
            XRect dstrect,
            XRect srcrect,
            XGraphicsUnit unit
        )
    {
        // Before saving, the current transformation matrix must be completely realized.
        BeginGraphicMode();
        RealizeTransform();
        _gfxState.InternalState = container.InternalState;
        SaveState();
    }

    public void EndContainer
        (
            XGraphicsContainer container
        )
    {
        BeginGraphicMode();
        RestoreState (container.InternalState);
    }

    #endregion

    // --------------------------------------------------------------------------------------------

    #region Transformation

    //public void SetPageTransform(XPageDirection direction, XPoint origion, XGraphicsUnit unit)
    //{
    //  if (_gfxStateStack.Count > 0)
    //    throw new InvalidOperationException("PageTransformation can be modified only when the graphics stack is empty.");

    //  throw new NotImplementedException("SetPageTransform");
    //}

    public XMatrix Transform
    {
        get
        {
            if (_gfxState.UnrealizedCtm.IsIdentity)
            {
                return _gfxState.EffectiveCtm;
            }

            return _gfxState.UnrealizedCtm * _gfxState.RealizedCtm;
        }
    }

    public void AddTransform (XMatrix value, XMatrixOrder matrixOrder)
    {
        _gfxState.AddTransform (value, matrixOrder);
    }

    #endregion

    // --------------------------------------------------------------------------------------------

    #region Clipping

    public void SetClip
        (
            XGraphicsPath path,
            XCombineMode combineMode
        )
    {
        if (path == null)
        {
            throw new NotImplementedException ("SetClip with no path.");
        }

        // Ensure that the graphics state stack level is at least 2, because otherwise an error
        // occurs when someone set the clip region before something was drawn.
        if (_gfxState.Level < GraphicsStackLevelWorldSpace)
        {
            RealizeTransform(); // TODO: refactor this function
        }

        if (combineMode == XCombineMode.Replace)
        {
            if (_clipLevel != 0)
            {
                if (_clipLevel != _gfxState.Level)
                {
                    throw new NotImplementedException (
                        "Cannot set new clip region in an inner graphic state level.");
                }
                else
                {
                    ResetClip();
                }
            }

            _clipLevel = _gfxState.Level;
        }
        else if (combineMode == XCombineMode.Intersect)
        {
            if (_clipLevel == 0)
            {
                _clipLevel = _gfxState.Level;
            }
        }
        else
        {
            Debug.Assert (false, "Invalid XCombineMode in internal function.");
        }

        _gfxState.SetAndRealizeClipPath (path);
    }

    /// <summary>
    /// Sets the clip path empty. Only possible if graphic state level has the same value as it has when
    /// the first time SetClip was invoked.
    /// </summary>
    public void ResetClip()
    {
        // No clip level means no clipping occurs and nothing is to do.
        if (_clipLevel == 0)
        {
            return;
        }

        // Only at the clipLevel the clipping can be reset.
        if (_clipLevel != _gfxState.Level)
        {
            throw new NotImplementedException ("Cannot reset clip region in an inner graphic state level.");
        }

        // Must be in graphical mode before popping the graphics state.
        BeginGraphicMode();

        // Save InternalGraphicsState and transformation of the current graphical state.
        var state = _gfxState.InternalState;
        var ctm = _gfxState.EffectiveCtm;

        // Empty clip path by switching back to the previous state.
        RestoreState();
        SaveState();

        // Save internal state
        _gfxState.InternalState = state;

        // Restore CTM
        // TODO: check rest of clip
        //GfxState.Transform = ctm;
    }

    /// <summary>
    /// The nesting level of the PDF graphics state stack when the clip region was set to non empty.
    /// Because of the way PDF is made the clip region can only be reset at this level.
    /// </summary>
    int _clipLevel;

    #endregion

    // --------------------------------------------------------------------------------------------

    #region Miscellaneous

    /// <summary>
    /// Writes a comment to the PDF content stream. May be useful for debugging purposes.
    /// </summary>
    public void WriteComment
        (
            string comment
        )
    {
        comment = comment.Replace ("\n", "\n% ");

        // TODO: Some more checks necessary?
        Append ("% " + comment + "\n");
    }

    #endregion

    // --------------------------------------------------------------------------------------------

    #region Append to PDF stream

    /// <summary>
    /// Appends one or up to five Bézier curves that interpolate the arc.
    /// </summary>
    void AppendPartialArc
        (
            double x,
            double y,
            double width,
            double height,
            double startAngle,
            double sweepAngle,
            PathStart pathStart,
            XMatrix matrix
        )
    {
        // Normalize the angles
        var α = startAngle;
        if (α < 0)
        {
            α = α + (1 + Math.Floor ((Math.Abs (α) / 360))) * 360;
        }
        else if (α > 360)
        {
            α = α - Math.Floor (α / 360) * 360;
        }

        Debug.Assert (α >= 0 && α <= 360);

        var β = sweepAngle;
        if (β < -360)
        {
            β = -360;
        }
        else if (β > 360)
        {
            β = 360;
        }

        if (α == 0 && β < 0)
        {
            α = 360;
        }
        else if (α == 360 && β > 0)
        {
            α = 0;
        }

        // Is it possible that the arc is small starts and ends in same quadrant?
        var smallAngle = Math.Abs (β) <= 90;

        β = α + β;
        if (β < 0)
        {
            β = β + (1 + Math.Floor ((Math.Abs (β) / 360))) * 360;
        }

        var clockwise = sweepAngle > 0;
        var startQuadrant = Quadrant (α, true, clockwise);
        var endQuadrant = Quadrant (β, false, clockwise);

        if (startQuadrant == endQuadrant && smallAngle)
        {
            AppendPartialArcQuadrant (x, y, width, height, α, β, pathStart, matrix);
        }
        else
        {
            var currentQuadrant = startQuadrant;
            var firstLoop = true;
            do
            {
                if (currentQuadrant == startQuadrant && firstLoop)
                {
                    double ξ = currentQuadrant * 90 + (clockwise ? 90 : 0);
                    AppendPartialArcQuadrant (x, y, width, height, α, ξ, pathStart, matrix);
                }
                else if (currentQuadrant == endQuadrant)
                {
                    double ξ = currentQuadrant * 90 + (clockwise ? 0 : 90);
                    AppendPartialArcQuadrant (x, y, width, height, ξ, β, PathStart.Ignore1st, matrix);
                }
                else
                {
                    double ξ1 = currentQuadrant * 90 + (clockwise ? 0 : 90);
                    double ξ2 = currentQuadrant * 90 + (clockwise ? 90 : 0);
                    AppendPartialArcQuadrant (x, y, width, height, ξ1, ξ2, PathStart.Ignore1st, matrix);
                }

                // Don't stop immediately if arc is greater than 270 degrees
                if (currentQuadrant == endQuadrant && smallAngle)
                {
                    break;
                }

                smallAngle = true;

                if (clockwise)
                {
                    currentQuadrant = currentQuadrant == 3 ? 0 : currentQuadrant + 1;
                }
                else
                {
                    currentQuadrant = currentQuadrant == 0 ? 3 : currentQuadrant - 1;
                }

                firstLoop = false;
            } while (true);
        }
    }

    /// <summary>
    /// Gets the quadrant (0 through 3) of the specified angle. If the angle lies on an edge
    /// (0, 90, 180, etc.) the result depends on the details how the angle is used.
    /// </summary>
    int Quadrant
        (
            double phi,
            bool start,
            bool clockwise
        )
    {
        Debug.Assert (phi >= 0);
        if (phi > 360)
        {
            phi = phi - Math.Floor (phi / 360) * 360;
        }

        var quadrant = (int)(phi / 90);
        if (quadrant * 90 == phi)
        {
            if ((start && !clockwise) || (!start && clockwise))
            {
                quadrant = quadrant == 0 ? 3 : quadrant - 1;
            }
        }
        else
        {
            quadrant = clockwise ? ((int)Math.Floor (phi / 90)) % 4 : (int)Math.Floor (phi / 90);
        }

        return quadrant;
    }

    /// <summary>
    /// Appends a Bézier curve for an arc within a quadrant.
    /// </summary>
    void AppendPartialArcQuadrant
        (
            double x,
            double y,
            double width,
            double height,
            double alpha,
            double beta,
            PathStart pathStart,
            XMatrix matrix
        )
    {
        Debug.Assert (alpha >= 0 && alpha <= 360);
        Debug.Assert (beta >= 0);
        if (beta > 360)
        {
            beta = beta - Math.Floor (beta / 360) * 360;
        }

        Debug.Assert (Math.Abs (alpha - beta) <= 90);

        // Scanling factor
        var δx = width / 2;
        var δy = height / 2;

        // Center of ellipse
        var x0 = x + δx;
        var y0 = y + δy;

        // We have the following quarters:
        //     |
        //   2 | 3
        // ----+-----
        //   1 | 0
        //     |
        // If the angles lie in quarter 2 or 3, their values are subtracted by 180 and the
        // resulting curve is reflected at the center. This algorithm works as expected (simply tried out).
        // There may be a mathematically more elegant solution...
        var reflect = false;
        if (alpha >= 180 && beta >= 180)
        {
            alpha -= 180;
            beta -= 180;
            reflect = true;
        }

        double sinα, sinβ;
        if (width == height)
        {
            // Circular arc needs no correction.
            alpha = alpha * Calc.Deg2Rad;
            beta = beta * Calc.Deg2Rad;
        }
        else
        {
            // Elliptic arc needs the angles to be adjusted such that the scaling transformation is compensated.
            alpha = alpha * Calc.Deg2Rad;
            sinα = Math.Sin (alpha);
            if (Math.Abs (sinα) > 1E-10)
            {
                alpha = Math.PI / 2 - Math.Atan (δy * Math.Cos (alpha) / (δx * sinα));
            }

            beta = beta * Calc.Deg2Rad;
            sinβ = Math.Sin (beta);
            if (Math.Abs (sinβ) > 1E-10)
            {
                beta = Math.PI / 2 - Math.Atan (δy * Math.Cos (beta) / (δx * sinβ));
            }
        }

        var κ = 4 * (1 - Math.Cos ((alpha - beta) / 2)) / (3 * Math.Sin ((beta - alpha) / 2));
        sinα = Math.Sin (alpha);
        var cosα = Math.Cos (alpha);
        sinβ = Math.Sin (beta);
        var cosβ = Math.Cos (beta);

        const string format = Config.SignificantFigures3;
        XPoint pt1, pt2, pt3;
        if (!reflect)
        {
            // Calculation for quarter 0 and 1
            switch (pathStart)
            {
                case PathStart.MoveTo1st:
                    pt1 = matrix.Transform (new XPoint (x0 + δx * cosα, y0 + δy * sinα));
                    AppendFormatPoint ("{0:" + format + "} {1:" + format + "} m\n", pt1.X, pt1.Y);
                    break;

                case PathStart.LineTo1st:
                    pt1 = matrix.Transform (new XPoint (x0 + δx * cosα, y0 + δy * sinα));
                    AppendFormatPoint ("{0:" + format + "} {1:" + format + "} l\n", pt1.X, pt1.Y);
                    break;

                case PathStart.Ignore1st:
                    break;
            }

            pt1 = matrix.Transform (new XPoint (x0 + δx * (cosα - κ * sinα), y0 + δy * (sinα + κ * cosα)));
            pt2 = matrix.Transform (new XPoint (x0 + δx * (cosβ + κ * sinβ), y0 + δy * (sinβ - κ * cosβ)));
            pt3 = matrix.Transform (new XPoint (x0 + δx * cosβ, y0 + δy * sinβ));
            AppendFormat3Points (
                "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format +
                "} {5:" + format + "} c\n",
                pt1.X, pt1.Y, pt2.X, pt2.Y, pt3.X, pt3.Y);
        }
        else
        {
            // Calculation for quarter 2 and 3.
            switch (pathStart)
            {
                case PathStart.MoveTo1st:
                    pt1 = matrix.Transform (new XPoint (x0 - δx * cosα, y0 - δy * sinα));
                    AppendFormatPoint ("{0:" + format + "} {1:" + format + "} m\n", pt1.X, pt1.Y);
                    break;

                case PathStart.LineTo1st:
                    pt1 = matrix.Transform (new XPoint (x0 - δx * cosα, y0 - δy * sinα));
                    AppendFormatPoint ("{0:" + format + "} {1:" + format + "} l\n", pt1.X, pt1.Y);
                    break;

                case PathStart.Ignore1st:
                    break;
            }

            pt1 = matrix.Transform (new XPoint (x0 - δx * (cosα - κ * sinα), y0 - δy * (sinα + κ * cosα)));
            pt2 = matrix.Transform (new XPoint (x0 - δx * (cosβ + κ * sinβ), y0 - δy * (sinβ - κ * cosβ)));
            pt3 = matrix.Transform (new XPoint (x0 - δx * cosβ, y0 - δy * sinβ));
            AppendFormat3Points (
                "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format +
                "} {5:" + format + "} c\n",
                pt1.X, pt1.Y, pt2.X, pt2.Y, pt3.X, pt3.Y);
        }
    }

    /// <summary>
    /// Appends a Bézier curve for a cardinal spline through pt1 and pt2.
    /// </summary>
    void AppendCurveSegment
        (
            XPoint pt0,
            XPoint pt1,
            XPoint pt2,
            XPoint pt3,
            double tension3
        )
    {
        const string format = Config.SignificantFigures4;
        AppendFormat3Points (
            "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format + "} {5:" +
            format + "} c\n",
            pt1.X + tension3 * (pt2.X - pt0.X), pt1.Y + tension3 * (pt2.Y - pt0.Y),
            pt2.X - tension3 * (pt3.X - pt1.X), pt2.Y - tension3 * (pt3.Y - pt1.Y),
            pt2.X, pt2.Y);
    }

    /// <summary>
    /// Appends the content of a GraphicsPath object.
    /// </summary>
    internal void AppendPath
        (
            CoreGraphicsPath path
        )
    {
        AppendPath (path.PathPoints, path.PathTypes);

        //XPoint[] points = path.PathPoints;
        //Byte[] types = path.PathTypes;

        //int count = points.Length;
        //if (count == 0)
        //    return;

        //for (int idx = 0; idx < count; idx++)
        //{
        //    // From GDI+ documentation:
        //    const byte PathPointTypeStart = 0; // move
        //    const byte PathPointTypeLine = 1; // line
        //    const byte PathPointTypeBezier = 3; // default Bezier (= cubic Bezier)
        //    const byte PathPointTypePathTypeMask = 0x07; // type mask (lowest 3 bits).
        //    //const byte PathPointTypeDashMode = 0x10; // currently in dash mode.
        //    //const byte PathPointTypePathMarker = 0x20; // a marker for the path.
        //    const byte PathPointTypeCloseSubpath = 0x80; // closed flag

        //    byte type = types[idx];
        //    switch (type & PathPointTypePathTypeMask)
        //    {
        //        case PathPointTypeStart:
        //            //PDF_moveto(pdf, points[idx].X, points[idx].Y);
        //            AppendFormat("{0:" + format + "} {1:" + format + "} m\n", points[idx].X, points[idx].Y);
        //            break;

        //        case PathPointTypeLine:
        //            //PDF_lineto(pdf, points[idx].X, points[idx].Y);
        //            AppendFormat("{0:" + format + "} {1:" + format + "} l\n", points[idx].X, points[idx].Y);
        //            if ((type & PathPointTypeCloseSubpath) != 0)
        //                Append("h\n");
        //            break;

        //        case PathPointTypeBezier:
        //            Debug.Assert(idx + 2 < count);
        //            //PDF_curveto(pdf, points[idx].X, points[idx].Y,
        //            //                 points[idx + 1].X, points[idx + 1].Y,
        //            //                 points[idx + 2].X, points[idx + 2].Y);
        //            AppendFormat("{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format + "} {5:" + format + "} c\n", points[idx].X, points[idx].Y,
        //                points[++idx].X, points[idx].Y, points[++idx].X, points[idx].Y);
        //            if ((types[idx] & PathPointTypeCloseSubpath) != 0)
        //                Append("h\n");
        //            break;
        //    }
        //}
    }

    private void AppendPath
        (
            IReadOnlyList<XPoint> points,
            IReadOnlyList<byte> types
        )
    {
        const string format = Config.SignificantFigures4;
        var count = points.Count;
        if (count == 0)
        {
            return;
        }

        for (var idx = 0; idx < count; idx++)
        {
            // ReSharper disable InconsistentNaming
            // From GDI+ documentation:
            const byte PathPointTypeStart = 0; // move
            const byte PathPointTypeLine = 1; // line
            const byte PathPointTypeBezier = 3; // default Bezier (= cubic Bezier)
            const byte PathPointTypePathTypeMask = 0x07; // type mask (lowest 3 bits).

            //const byte PathPointTypeDashMode = 0x10; // currently in dash mode.
            //const byte PathPointTypePathMarker = 0x20; // a marker for the path.
            const byte PathPointTypeCloseSubpath = 0x80; // closed flag

            // ReSharper restore InconsistentNaming

            var type = types[idx];
            switch (type & PathPointTypePathTypeMask)
            {
                case PathPointTypeStart:
                    //PDF_moveto(pdf, points[idx].X, points[idx].Y);
                    AppendFormatPoint ("{0:" + format + "} {1:" + format + "} m\n", points[idx].X, points[idx].Y);
                    break;

                case PathPointTypeLine:
                    //PDF_lineto(pdf, points[idx].X, points[idx].Y);
                    AppendFormatPoint ("{0:" + format + "} {1:" + format + "} l\n", points[idx].X, points[idx].Y);
                    if ((type & PathPointTypeCloseSubpath) != 0)
                    {
                        Append ("h\n");
                    }

                    break;

                case PathPointTypeBezier:
                    Debug.Assert (idx + 2 < count);

                    //PDF_curveto(pdf, points[idx].X, points[idx].Y,
                    //                 points[idx + 1].X, points[idx + 1].Y,
                    //                 points[idx + 2].X, points[idx + 2].Y);
                    AppendFormat3Points (
                        "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format +
                        "} {5:" + format + "} c\n", points[idx].X, points[idx].Y,
                        points[++idx].X, points[idx].Y, points[++idx].X, points[idx].Y);
                    if ((types[idx] & PathPointTypeCloseSubpath) != 0)
                    {
                        Append ("h\n");
                    }

                    break;
            }
        }
    }

    internal void Append
        (
            string value
        )
    {
        _content.Append (value);
    }

    internal void AppendFormatArgs
        (
            string format,
            params object[] args
        )
    {
        _content.AppendFormat (CultureInfo.InvariantCulture, format, args);
#if DEBUG
        var dummy = _content.ToString();
        dummy = dummy.Substring (Math.Max (0, dummy.Length - 100));
        dummy.GetType().NotUsed();
#endif
    }

    internal void AppendFormatString (string format, string s)
    {
        _content.AppendFormat (CultureInfo.InvariantCulture, format, s);
    }

    internal void AppendFormatFont (string format, string s, double d)
    {
        _content.AppendFormat (CultureInfo.InvariantCulture, format, s, d);
    }

    internal void AppendFormatInt (string format, int n)
    {
        _content.AppendFormat (CultureInfo.InvariantCulture, format, n);
    }

    internal void AppendFormatDouble (string format, double d)
    {
        _content.AppendFormat (CultureInfo.InvariantCulture, format, d);
    }

    internal void AppendFormatPoint (string format, double x, double y)
    {
        var result = WorldToView (new XPoint (x, y));
        _content.AppendFormat (CultureInfo.InvariantCulture, format, result.X, result.Y);
    }

    internal void AppendFormatRect (string format, double x, double y, double width, double height)
    {
        var point1 = WorldToView (new XPoint (x, y));
        _content.AppendFormat (CultureInfo.InvariantCulture, format, point1.X, point1.Y, width, height);
    }

    internal void AppendFormat3Points (string format, double x1, double y1, double x2, double y2, double x3,
        double y3)
    {
        var point1 = WorldToView (new XPoint (x1, y1));
        var point2 = WorldToView (new XPoint (x2, y2));
        var point3 = WorldToView (new XPoint (x3, y3));
        _content.AppendFormat (CultureInfo.InvariantCulture, format, point1.X, point1.Y, point2.X, point2.Y,
            point3.X, point3.Y);
    }

    internal void AppendFormat
        (
            string format,
            XPoint point
        )
    {
        var result = WorldToView (point);
        _content.AppendFormat (CultureInfo.InvariantCulture, format, result.X, result.Y);
    }

    internal void AppendFormat
        (
        string format,
        double x,
        double y,
        string s
        )
    {
        var result = WorldToView (new XPoint (x, y));
        _content.AppendFormat (CultureInfo.InvariantCulture, format, result.X, result.Y, s);
    }

    internal void AppendFormatImage
        (
            string format,
            double x,
            double y,
            double width,
            double height,
            string name
        )
    {
        var result = WorldToView (new XPoint (x, y));
        _content.AppendFormat (CultureInfo.InvariantCulture, format, result.X, result.Y, width, height, name);
    }

    void AppendStrokeFill
        (
            XPen pen,
            XBrush? brush,
            XFillMode fillMode,
            bool closePath
        )
    {
        if (closePath)
        {
            _content.Append ("h ");
        }

        if (fillMode == XFillMode.Winding)
        {
            if (pen != null && brush != null)
            {
                _content.Append ("B\n");
            }
            else if (pen != null)
            {
                _content.Append ("S\n");
            }
            else
            {
                _content.Append ("f\n");
            }
        }
        else
        {
            if (pen != null && brush != null)
            {
                _content.Append ("B*\n");
            }
            else if (pen != null)
            {
                _content.Append ("S\n");
            }
            else
            {
                _content.Append ("f*\n");
            }
        }
    }

    #endregion

    // --------------------------------------------------------------------------------------------

    #region Realizing graphical state

    /// <summary>
    /// Initializes the default view transformation, i.e. the transformation from the user page
    /// space to the PDF page space.
    /// </summary>
    void BeginPage()
    {
        if (_gfxState.Level == GraphicsStackLevelInitial)
        {
            // TODO: Is PageOriging and PageScale (== Viewport) useful? Or just public DefaultViewMatrix (like Presentation Manager has had)
            // May be a BeginContainer(windows, viewport) is useful for userer that are not familar with maxtrix transformations.

            // Flip page horizontally and mirror text.

            // PDF uses a standard right-handed Cartesian coordinate system with the y axis directed up
            // and the rotation counterclockwise. Windows uses the opposite convertion with y axis
            // directed down and rotation clockwise. When I started with PDFsharp I flipped pages horizontally
            // and then mirrored text to compensate the effect that the fipping turns text upside down.
            // I found this technique during analysis of PDF documents generated with PDFlib. Unfortunately
            // this technique leads to several problems with programms that compose or view PDF documents
            // generated with PdfSharpCore.
            // In PDFsharp 1.4 I implement a revised technique that does not need text mirroring any more.

            DefaultViewMatrix = new XMatrix();
            if (Graphics!.PageDirection == XPageDirection.Downwards)
            {
                // Take TrimBox into account.
                PageHeightPt = Size.Height;
                var trimOffset = new XPoint();
                if (_page != null && _page.TrimMargins.AreSet)
                {
                    PageHeightPt += _page.TrimMargins.Top.Point + _page.TrimMargins.Bottom.Point;
                    trimOffset = new XPoint (_page.TrimMargins.Left.Point, _page.TrimMargins.Top.Point);
                }

                // Scale with page units.
                switch (Graphics.PageUnit)
                {
                    case XGraphicsUnit.Point:
                        // Factor is 1.
                        // DefaultViewMatrix.ScalePrepend(XUnit.PointFactor);
                        break;

                    case XGraphicsUnit.Presentation:
                        DefaultViewMatrix.ScalePrepend (XUnit.PresentationFactor);
                        break;

                    case XGraphicsUnit.Inch:
                        DefaultViewMatrix.ScalePrepend (XUnit.InchFactor);
                        break;

                    case XGraphicsUnit.Millimeter:
                        DefaultViewMatrix.ScalePrepend (XUnit.MillimeterFactor);
                        break;

                    case XGraphicsUnit.Centimeter:
                        DefaultViewMatrix.ScalePrepend (XUnit.CentimeterFactor);
                        break;
                }

                if (trimOffset != new XPoint())
                {
                    Debug.Assert (Graphics.PageUnit == XGraphicsUnit.Point,
                        "With TrimMargins set the page units must be Point. Ohter cases nyi.");
                    DefaultViewMatrix.TranslatePrepend (trimOffset.X, -trimOffset.Y);
                }

                // Save initial graphic state.
                SaveState();

                // Set default page transformation, if any.
                if (!DefaultViewMatrix.IsIdentity)
                {
                    Debug.Assert (_gfxState.RealizedCtm.IsIdentity);

                    //_gfxState.RealizedCtm = DefaultViewMatrix;
                    const string format = Config.SignificantFigures7;
                    var cm = DefaultViewMatrix.GetElements();
                    AppendFormatArgs (
                        "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format +
                        "} {5:" + format + "} cm ",
                        cm[0], cm[1], cm[2], cm[3], cm[4], cm[5]);
                }

                // Set page transformation
                //double[] cm = DefaultViewMatrix.GetElements();
                //AppendFormat("{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format + "} {5:" + format + "} cm ",
                //  cm[0], cm[1], cm[2], cm[3], cm[4], cm[5]);
            }
            else
            {
                // Scale with page units.
                switch (Graphics.PageUnit)
                {
                    case XGraphicsUnit.Point:
                        // Factor is 1.
                        // DefaultViewMatrix.ScalePrepend(XUnit.PointFactor);
                        break;

                    case XGraphicsUnit.Presentation:
                        DefaultViewMatrix.ScalePrepend (XUnit.PresentationFactor);
                        break;

                    case XGraphicsUnit.Inch:
                        DefaultViewMatrix.ScalePrepend (XUnit.InchFactor);
                        break;

                    case XGraphicsUnit.Millimeter:
                        DefaultViewMatrix.ScalePrepend (XUnit.MillimeterFactor);
                        break;

                    case XGraphicsUnit.Centimeter:
                        DefaultViewMatrix.ScalePrepend (XUnit.CentimeterFactor);
                        break;
                }

                // Save initial graphic state.
                SaveState();

                // Set page transformation.
                const string format = Config.SignificantFigures7;
                var cm = DefaultViewMatrix.GetElements();
                AppendFormat3Points (
                    "{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format +
                    "} {5:" + format + "} cm ",
                    cm[0], cm[1], cm[2], cm[3], cm[4], cm[5]);
            }
        }
    }

    /// <summary>
    /// Ends the content stream, i.e. ends the text mode and balances the graphic state stack.
    /// </summary>
    void EndPage()
    {
        if (_streamMode == StreamMode.Text)
        {
            _content.Append ("ET\n");
            _streamMode = StreamMode.Graphic;
        }

        while (_gfxStateStack.Count != 0)
            RestoreState();
    }

    /// <summary>
    /// Begins the graphic mode (i.e. ends the text mode).
    /// </summary>
    internal void BeginGraphicMode()
    {
        if (_streamMode != StreamMode.Graphic)
        {
            if (_streamMode == StreamMode.Text)
            {
                _content.Append ("ET\n");
            }

            _streamMode = StreamMode.Graphic;
        }
    }

    /// <summary>
    /// Begins the graphic mode (i.e. ends the text mode).
    /// </summary>
    internal void BeginTextMode()
    {
        if (_streamMode != StreamMode.Text)
        {
            _streamMode = StreamMode.Text;
            _content.Append ("BT\n");

            // Text matrix is empty after BT
            _gfxState.RealizedTextPosition = new XPoint();
            _gfxState.ItalicSimulationOn = false;
        }
    }

    StreamMode _streamMode;

    /// <summary>
    /// Makes the specified pen and brush to the current graphics objects.
    /// </summary>
    private void Realize (XPen? pen, XBrush? brush)
    {
        BeginPage();
        BeginGraphicMode();
        RealizeTransform();

        if (pen is not null)
        {
            _gfxState.RealizePen (pen, _colorMode); // page.document.Options.ColorMode);
        }

        if (brush is not null)
        {
            // Render mode is 0 except for bold simulation.
            _gfxState.RealizeBrush (brush, _colorMode, 0, 0); // page.document.Options.ColorMode);
        }
    }

    /// <summary>
    /// Makes the specified pen to the current graphics object.
    /// </summary>
    void Realize (XPen pen)
    {
        Realize (pen, null);
    }

    /// <summary>
    /// Makes the specified brush to the current graphics object.
    /// </summary>
    void Realize (XBrush brush)
    {
        Realize (null, brush);
    }

    /// <summary>
    /// Makes the specified font and brush to the current graphics objects.
    /// </summary>
    void Realize
        (
            XFont font,
            XBrush brush,
            int renderingMode
        )
    {
        BeginPage();
        RealizeTransform();
        BeginTextMode();
        _gfxState.RealizeFont (font, brush, renderingMode);
    }

    /// <summary>
    /// PDFsharp uses the Td operator to set the text position. Td just sets the offset of the text matrix
    /// and produces lesser code as Tm.
    /// </summary>
    /// <param name="pos">The absolute text position.</param>
    /// <param name="dy">The dy.</param>
    /// <param name="adjustSkew">true if skewing for italic simulation is currently on.</param>
    void AdjustTdOffset
        (
            ref XPoint pos,
            double dy,
            bool adjustSkew
        )
    {
        pos.Y += dy;

        // Reference: TABLE 5.5  Text-positioning operators / Page 406
        var posSave = pos;

        // Map from absolute to relative position.
        pos -= new XVector (_gfxState.RealizedTextPosition.X, _gfxState.RealizedTextPosition.Y);
        if (adjustSkew)
        {
            // In case that italic simulation is on X must be adjusted according to Y offset. Weird but works :-)
            pos.X -= Const.ItalicSkewAngleSinus * pos.Y;
        }

        _gfxState.RealizedTextPosition = posSave;
    }

    /// <summary>
    /// Makes the specified image to the current graphics object.
    /// </summary>
    string Realize
        (
            XImage image
        )
    {
        BeginPage();
        BeginGraphicMode();
        RealizeTransform();

        // The transparency set for a brush also applies to images. Set opacity to 100% so image will be drawn without transparency.
        _gfxState.RealizeNonStrokeTransparency (1, _colorMode);

        return image is XForm form ? GetFormName (form) : GetImageName (image);
    }

    /// <summary>
    /// Realizes the current transformation matrix, if necessary.
    /// </summary>
    void RealizeTransform()
    {
        BeginPage();

        if (_gfxState.Level == GraphicsStackLevelPageSpace)
        {
            BeginGraphicMode();
            SaveState();
        }

        //if (gfxState.MustRealizeCtm)
        if (!_gfxState.UnrealizedCtm.IsIdentity)
        {
            BeginGraphicMode();
            _gfxState.RealizeCtm();
        }
    }

    /// <summary>
    /// Convert a point from Windows world space to PDF world space.
    /// </summary>
    internal XPoint WorldToView
        (
            XPoint point
        )
    {
        // If EffectiveCtm is not yet realized InverseEffectiveCtm is invalid.
        Debug.Assert (_gfxState.UnrealizedCtm.IsIdentity, "Somewhere a RealizeTransform is missing.");
#if true

        // See in #else case why this is correct.
        var pt = _gfxState.WorldTransform.Transform (point);
        return _gfxState.InverseEffectiveCtm.Transform (new XPoint (pt.X,
            PageHeightPt / DefaultViewMatrix.M22 - pt.Y));
#else
            // Get inverted PDF world transform matrix.
            XMatrix invers = _gfxState.EffectiveCtm;
            invers.Invert();

            // Apply transform in Windows world space.
            XPoint pt1 = _gfxState.WorldTransform.Transform(point);
#if true
            // Do the transformation (see #else case) in one step.
            XPoint pt2 = new XPoint(pt1.X, PageHeightPt / DefaultViewMatrix.M22 - pt1.Y);
#else
            // Replicable version

            // Apply default transformation.
            pt1.X = pt1.X * DefaultViewMatrix.M11;
            pt1.Y = pt1.Y * DefaultViewMatrix.M22;

            // Convert from Windows space to PDF space.
            XPoint pt2 = new XPoint(pt1.X, PageHeightPt - pt1.Y);

            pt2.X = pt2.X / DefaultViewMatrix.M11;
            pt2.Y = pt2.Y / DefaultViewMatrix.M22;
#endif
            XPoint pt3 = invers.Transform(pt2);
            return pt3;
#endif
    }

    #endregion

    /// <summary>
    /// Gets the owning PdfDocument of this page or form.
    /// </summary>
    internal PdfDocument? Owner
    {
        get
        {
            if (_page != null)
            {
                return _page.Owner;
            }

            return _form!.Owner;
        }
    }

    internal XGraphics? Graphics { get; private set; }

    /// <summary>
    /// Gets the PdfResources of this page or form.
    /// </summary>
    internal PdfResources Resources
    {
        get
        {
            if (_page != null)
            {
                return _page.Resources;
            }

            return _form!.Resources;
        }
    }

    /// <summary>
    /// Gets the size of this page or form.
    /// </summary>
    internal XSize Size
    {
        get
        {
            if (_page != null)
            {
                return new XSize (_page.Width, _page.Height);
            }

            return _form!.Size;
        }
    }

    /// <summary>
    /// Gets the resource name of the specified font within this page or form.
    /// </summary>
    internal string GetFontName (XFont font, out PdfFont pdfFont)
    {
        if (_page != null)
        {
            return _page.GetFontName (font, out pdfFont);
        }

        return _form!.GetFontName (font, out pdfFont);
    }

    /// <summary>
    /// Gets the resource name of the specified image within this page or form.
    /// </summary>
    internal string GetImageName (XImage image)
    {
        if (_page != null)
        {
            return _page.GetImageName (image);
        }

        return _form!.GetImageName (image);
    }

    /// <summary>
    /// Gets the resource name of the specified form within this page or form.
    /// </summary>
    internal string GetFormName (XForm form)
    {
        return _page is not null
            ? _page.GetFormName (form)
            : _form.ThrowIfNull().GetFormName (form);
    }

    internal PdfPage? _page;
    internal XForm? _form;
    internal PdfColorMode _colorMode;
    private readonly StringBuilder _content;

    /// <summary>
    /// The q/Q nesting level is 0.
    /// </summary>
    const int GraphicsStackLevelInitial = 0;

    /// <summary>
    /// The q/Q nesting level is 1.
    /// </summary>
    const int GraphicsStackLevelPageSpace = 1;

    /// <summary>
    /// The q/Q nesting level is 2.
    /// </summary>
    const int GraphicsStackLevelWorldSpace = 2;

    #region PDF Graphics State

    /// <summary>
    /// Saves the current graphical state.
    /// </summary>
    void SaveState()
    {
        Debug.Assert (_streamMode == StreamMode.Graphic, "Cannot save state in text mode.");

        _gfxStateStack.Push (_gfxState);
        _gfxState = _gfxState.Clone();
        _gfxState.Level = _gfxStateStack.Count;
        Append ("q\n");
    }

    /// <summary>
    /// Restores the previous graphical state.
    /// </summary>
    void RestoreState()
    {
        Debug.Assert (_streamMode == StreamMode.Graphic, "Cannot restore state in text mode.");

        _gfxState = _gfxStateStack.Pop();
        Append ("Q\n");
    }

    PdfGraphicsState RestoreState (InternalGraphicsState state)
    {
        var count = 1;
        var top = _gfxStateStack.Pop();
        while (top.InternalState != state)
        {
            Append ("Q\n");
            count++;
            top = _gfxStateStack.Pop();
        }

        Append ("Q\n");
        _gfxState = top;
        return top;
    }

    /// <summary>
    /// The current graphical state.
    /// </summary>
    PdfGraphicsState _gfxState;

    /// <summary>
    /// The graphical state stack.
    /// </summary>
    readonly Stack<PdfGraphicsState> _gfxStateStack = new ();

    #endregion

    /// <summary>
    /// The height of the PDF page in point including the trim box.
    /// </summary>
    public double PageHeightPt;

    /// <summary>
    /// The final transformation from the world space to the default page space.
    /// </summary>
    public XMatrix DefaultViewMatrix;
}
